package club.kingon.sql.builder.spring;

import club.kingon.sql.builder.inner.ClassMetadata;
import club.kingon.sql.builder.inner.ObjectMapperUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;
import java.lang.reflect.*;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author dragons
 * @date 2021/12/14 10:09
 */
public class MapperFactoryBean<T> implements FactoryBean<T>, EnvironmentAware {

    private Class<T> mapperInterface;

    private Environment environment;

    private String dataSourceConfigPrefix;

    private final static String DEFAULT_SUPPORTED_DATA_SOURCE = "com.zaxxer.hikari.HikariDataSource";

    private static final Map<String, JdbcTemplate> jdbcTemplateMap = new ConcurrentHashMap<>();

    private final static Logger log = LoggerFactory.getLogger(MapperFactoryBean.class);

    public MapperFactoryBean() {
        // intentionally empty
    }

    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public T getObject() throws Exception {
        JdbcTemplate jdbcTemplate = getJdbcTemplate();
        Class<?> modelClass = getActualTypeArguments0();
        T bean = (T) Proxy.newProxyInstance(
            getClass().getClassLoader(), new Class<?>[]{mapperInterface}, new JdbcBaseMapperJdkProxyHandler(modelClass, jdbcTemplate)
        );
        return bean;
    }

    private JdbcTemplate getJdbcTemplate() {
        String url = Objects.requireNonNull(getEnvironmentProperty(dataSourceConfigPrefix, String.class, "url"));
        String username = getEnvironmentProperty(dataSourceConfigPrefix, String.class, "username");
        String password = getEnvironmentProperty(dataSourceConfigPrefix, String.class, "password");
        String driverClassName = Objects.requireNonNull(getEnvironmentProperty(dataSourceConfigPrefix, String.class, "driver-class-name", "driverClassName"));
        String type = getEnvironmentPropertyDefault(dataSourceConfigPrefix, String.class, DEFAULT_SUPPORTED_DATA_SOURCE, "type");
        Class<? extends DataSource> dataSourceType;
        try {
            dataSourceType = (Class<? extends DataSource>) Class.forName(type);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Data source config type class cannot be found.");
        }
        String key = url + "`" + username + "`" + password + "`" + driverClassName;
        JdbcTemplate jdbcTemplate = jdbcTemplateMap.get(key);
        if (jdbcTemplate == null) {
            synchronized (jdbcTemplateMap) {
                jdbcTemplate = jdbcTemplateMap.get(key);
                if (jdbcTemplate == null) {
                    DataSource dataSource = DataSourceBuilder.create()
                        .url(url)
                        .type(dataSourceType)
                        .driverClassName(driverClassName)
                        .username(username)
                        .password(password)
                        .build();
                    injectDataSourceConfig(dataSourceConfigPrefix + ".pool", dataSource);
                    jdbcTemplate = new JdbcTemplate(dataSource);
                    jdbcTemplateMap.put(key, jdbcTemplate);
                }
            }
        }
        return jdbcTemplate;
    }

    private void injectDataSourceConfig(String prefix, DataSource dataSource) {
        ClassMetadata meta = new ClassMetadata(dataSource.getClass());
        Object val;
        for (Method method : meta.getSetterMethods()) {
            String methodName = method.getName();
            Class<?> parameterType = method.getParameterTypes()[0];
            String configName1, configName2;
            if (methodName.length() > 4) {
                configName1 = methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
            } else {
                configName1 = methodName.substring(3);
            }
            configName2 = ObjectMapperUtils.humpNameToUnderlinedName(configName1, "-");
            if ((val = getEnvironmentProperty(prefix, parameterType, configName1, configName2)) != null) {
                try {
                    method.invoke(dataSource, val);
                } catch (IllegalAccessException | InvocationTargetException e) {
                    throw new RuntimeException("Init data source fail.", e);
                }
            }
        }
    }

    private <T> T getEnvironmentPropertyDefault(String prefix, Class<?> type, T defaultValue, String ...keys) {
        T propertyValue = (T) getEnvironmentProperty(prefix, type, keys);
        if (propertyValue == null) return defaultValue;
        return propertyValue;
    }

    private <T> T getEnvironmentProperty(String prefix, Class<T> type, String ...keys) {
        T returnObj;
        for (String key : keys) {
            if ((returnObj = environment.getProperty(prefix + "." + key, type)) != null) {
                return returnObj;
            }
        }
        return null;
    }


    private Class<?> getActualTypeArguments0() {
        Class<?> clazz = mapperInterface;
        Type[] genericInterfaces = clazz.getGenericInterfaces();
        for (Type genericInterface : genericInterfaces) {
            if (genericInterface instanceof ParameterizedType) {
                return (Class<?>) ((ParameterizedType) genericInterface).getActualTypeArguments()[0];
            }
        }
        Type genericSuperclass = clazz.getGenericSuperclass();
        if (genericSuperclass instanceof ParameterizedType) {
            return (Class<?>) ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
        }
        return null;
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    public void setMapperInterface(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public Class<T> getMapperInterface() {
        return mapperInterface;
    }

    public void setDataSourceConfigPrefix(String dataSourceConfigPrefix) {
        this.dataSourceConfigPrefix = dataSourceConfigPrefix;
    }


    public String getDataSourceConfigPrefix() {
        return dataSourceConfigPrefix;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
}
